#include "simulationTime.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>

namespace hms
{
	
Time::Time( scalar end ){
	endAt(end);
	m_current = settings.start;
	if (settings.useExplicitFirstStep)
		m_step = settings.initialStep;
}

/**
 * @brief Constructs a new Time object from minimal input.
 * 
 * @param end The simulation end time in seconds.
 * @param writeTimeInterval The simulation time interval in seconds at which to
 * write output data.
 */
Time::Time( scalar end, scalar writeTimeIntervalArg ) :
	Time(end)
{
	writeTimeInterval(writeTimeIntervalArg);
}


/**
 * @brief Constructs a Time object from full input, e.g. when resuming a
 * simulation.
 * 
 * @param start The start or resume time of the simulation in seconds.
 * @param end The simulation end time in seconds.
 * @param maxTimeStep The maximum time step allowed in seconds.
 * @param writeTimeInterval The simulation time interval at which to write
 * output data.
 */
Time::Time(
	scalar start,
	scalar end,
	scalar maxTimeStepArg,
	scalar writeTimeInterval
) :
	Time(end, writeTimeInterval)
{
	startAt(start);
	maxTimeStep(maxTimeStepArg);
}

/**
 * @brief Constructs a Time object from full input, e.g. when resuming a
 * simulation. This version is for specifying a number of steps at which to
 * write output data.
 * 
 * @param start The start or resume time of the simulation in seconds.
 * @param end The simulation end time in seconds.
 * @param maxTimeStep The maximum time step allowed in seconds.
 * @param writeStepInterval The number of time steps between writing output data.
 */
Time::Time(
	scalar start,
	scalar end,
	scalar maxTimeStepArg,
	long int writeStepIntervalArg
) :
	Time(end)
{
	startAt(start);
	maxTimeStep(maxTimeStepArg);
	settings.writeAtTimeIntervals = false;
	writeStepInterval(writeStepIntervalArg);
}

Time::Time( TimeSettings&& settingsArg ){
	settings = std::move(settingsArg);
	m_current = settings.start;
	if (settings.useExplicitFirstStep)
		m_step = settings.initialStep;
}

/** @brief Returns the current simulation time in seconds. */
scalar Time::current() const {
	assert( m_current != signalCode::uninitialised &&
		"Current time is not set!" );
	return m_current;
}

/** @brief Returns the simulation end time in seconds. */
scalar Time::end() const {
	assert( settings.end != signalCode::uninitialised &&
		"End time is not set!" );
	return settings.end;
}

/** @brief Returns the maximum time step allowed in seconds. Default is numeric
 * maximum of the data type, i.e. no restriction. */
scalar Time::maxTimeStep() const { return settings.maxTimeStep; }

/** @brief Returns true if output is written at specific time intervals and 
 * false if output is written after a specific number of time steps. */
bool Time::writeAtTimeIntervals() const {
	return settings.writeAtTimeIntervals;
}

/** @brief Returns the time interval in seconds at which output is written. */
scalar Time::writeTimeInterval() const {
	assert( settings.writeAtTimeIntervals);
	assert( settings.writeTimeInterval != signalCode::uninitialised &&
		"Write time interval not set!" );
	return settings.writeTimeInterval;
}

/** @brief Returns the number of time steps after which output data is written.
 */
long int Time::writeStepInterval() const {
	assert( !settings.writeAtTimeIntervals );
	assert( settings.writeStepInterval != signalCode::uninitialised &&
		"Write step interval not set!" );
	return settings.writeStepInterval;
}

/** @brief Returns the stored number of times that output has been written  
*/
int Time::nWritten() const {
	return m_nWritten;
}

/** @brief Returns the number of time steps since the start or continuation 
 * of the simulation.*/
long int Time::nSteps() const { return m_nSteps; }

/**
 * @brief Returns the most recent time step determined by advance()
 * 
 * @return scalar 
 * 
 * @see advance()
 */
scalar Time::step() const {
	return m_step;
}

/** @brief Sets the time step to up to the one passed in.
 * Restrictions by the maximum time step and the write time are applied.
 * 
 * @param timeStep The desired time step in seconds.
 * 
 * @see step()
 * @see advance()
 * @see advanceBy()
 */
void Time::step( scalar timeStep ){
	assert( timeStep > 0 && "Time step must be strictly positive!" );

	/* While the timeStep argument is the maximum the solver allows for 
	 * stability, m_maxTimeStep may be set as an additional restriction for
	 * accuracy. */
	m_step = std::min(settings.maxTimeStep, timeStep);
	/* due to floating point precision loss, time can get stuck at a write time,
	 * so we check whether that's the case */
	if ( settings.writeAtTimeIntervals ){
		/* If the output is supposed to be written at specific time intervals,
		 * the time step may have to be reduced to land at a multiple of that
		 * interval. */
		scalar nextWriteTime { settings.writeTimeInterval * (nWritten()+1) };
		m_step = std::min( m_step, nextWriteTime - m_current );
	}
	/* no overshoot over the end time, and no negative time steps */
	if ( m_current + m_step > end() + epsilon )
		m_step = std::max(settings.end - m_current, epsilon );
}

/** @brief Returns true if the simulation end time is reached and false
 * otherwise.
 */
bool Time::atStart() const { return m_nSteps == 0; }

/** @brief Returns true if the simulation end time is reached and false
 * otherwise.
 */
bool Time::atEnd() const {
	return
		current() >= end()
		#ifndef NDEBUG
			|| nSteps() >= maxSteps()
		#endif
	;
}

/** @brief Returns true if output data must be written now and false otherwise.
 * */
bool Time::atWriteTime() const {
	return
		(
			settings.writeAtTimeIntervals &&
			std::abs( remainder( current(), writeTimeInterval() ) ) <= epsilon
		) ||
		( 
			!settings.writeAtTimeIntervals &&
			(m_nSteps % writeStepInterval() == 0)
		) || atEnd();
}

bool Time::isInitialised() const {
	return settings.end != signalCode::uninitialised &&
	(settings.writeAtTimeIntervals ?
		settings.writeTimeInterval != signalCode::uninitialised :
		settings.writeStepInterval != signalCode::uninitialised
	);
}

/**
 * @brief Sets the end time of the simulation.
 * 
 * @param end End time in seconds.
 * 
 * Anything goes. Negative end times, end times before current time, etc...
 */
void Time::endAt( scalar end ){
	settings.end = end;
}

/**
 * @brief Sets the maximum time step of the simulation.
 * 
 * @param maxTimeStep Maximum time step in seconds.
 */
void Time::maxTimeStep( scalar maxTimeStep){
	assert( maxTimeStep > 0 && "Max. time step must be strictly positive!" );
	settings.maxTimeStep = maxTimeStep;
}

/**
 * @brief Sets the current simulation time.
 * 
 * @param start The simulation start time in seconds.
 * 
 * Any value is valid. Negative values are allowed.
 */
void Time::startAt( scalar startArg ){
	settings.start = startArg;
	m_current = startArg;
}

/**
 * @brief Sets the flag for whether to write output data at specific time
 * intervals or after a specific number of time steps.
 * 
 * @param flag Pass true to let output data be written at specific time
 * intervals or false to let output data be written after a specific number of
 * time steps.
 */
void Time::writeAtTimeIntervals( bool flag ){
	settings.writeAtTimeIntervals = flag;
}

/**
 * @brief Sets the write time interval at which output data is written.
 * 
 * @param time The time interval in seconds.
 */
void Time::writeTimeInterval( scalar time ){
	assert( settings.writeAtTimeIntervals && "Setting has no effect!" );
	assert( time > 0 && "Write time interval must be strictly positive!" );
	settings.writeTimeInterval = time;
}

/**
 * @brief Sets the number of time steps after which output data is written.
 */
void Time::writeStepInterval( long int steps ){
	assert( !settings.writeAtTimeIntervals && "Setting has no effect!" );
	assert( steps > 0 && "Number of steps must be strictly positive!" );
	settings.writeStepInterval = steps;
}

/** @brief Advances the simulation time by up to the time step passed in.
 * Restrictions by the maximum time step and the write time are applied.
 * 
 * @param timeStep The desired time step in seconds by which to advance the
 * simulation.
 * 
 * @see step(scalar)
 * @see advance()
 */
void Time::advanceBy( scalar timeStep ){
	step( timeStep );
	advance();
}

/** @brief Advances the simulation time by the current time step.
 * 
 * @see advanceBy()
 * @see step()
 */
void Time::advance(){
	m_current += m_step;
	++m_nSteps;
	/* atWriteTime() also returns true if atEnd() does. Therefore the order here
	 * is important, because we always want to increment m_nWritten at the end,
	 * but do not want to do so twice */
	if ( atEnd() ){
		++m_nWritten;
	} else if ( atWriteTime() ){
		if ( settings.writeAtTimeIntervals ){
			m_nWritten = std::round( current() / writeTimeInterval() );
		} else
			m_nWritten = m_nSteps / settings.writeStepInterval;
	}
}

/**
 * @brief Prints the current time using the provided logger. Log level used is
 * "info".
 * 
 * @param logger 
 */
void Time::print( log::logger& logger ){
	logger.info( "Time: {0:>6.3f}", m_current );
}

void Time::useExplicitFirstStep(bool arg){
	settings.useExplicitFirstStep = arg;
}

bool Time::useExplicitFirstStep() const {
	return settings.useExplicitFirstStep;
}


#ifndef NDEBUG
Time::Time(
	UseMaxSteps,
	long maxStepsArg
) :
	Time( std::numeric_limits<long>::max() )
{
	maxSteps(maxStepsArg);
}

long Time::maxSteps() const { return settings.maxSteps; }
void Time::maxSteps(long arg){
	settings.maxSteps = arg;
}
#endif


} // end namespace hms